package org.owasp.esapi.crypto;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;

import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Logger;

/**
 * A class representing plaintext (versus ciphertext) as related to
 * cryptographic systems.  This class embodies UTF-8 byte-encoding to
 * translate between byte arrays and {@code String}s. Once constructed, this
 * object is immutable.
 * <p>
 * Note: Conversion to/from UTF-8 byte-encoding can, in theory, throw
 * an {@code UnsupportedEncodingException}. However, UTF-8 encoding
 * should be a standard encoding for all Java installations, so an
 * {@code UnsupportedEncodingException} never actually be thrown. Therefore,
 * in order to to keep client code uncluttered, any possible
 * {@code UnsupportedEncodingException}s will be first logged, and then
 * re-thrown as a {@code RuntimeException} with the original
 * {@code UnsupportedEncodingException} as the cause.
 * <p>
 * Copyright &copy; 2009 - The OWASP Foundation
 * </p>
 * @author kevin.w.wall@gmail.com
 * @see CipherText
 * @since 2.0
 */
public final class PlainText implements Serializable {

    private static final long serialVersionUID = 20090921;
    private static Logger logger = ESAPI.getLogger("PlainText");

    private byte[] rawBytes = null;

    /**
     * Construct a {@code PlainText} object from a {@code String}.
     * @param str    The {@code String} that is converted to a UTF-8 encoded
     *                 byte array to create the {@code PlainText} object.
     * @throws IllegalArgumentException    If {@code str} argument is null.
     */
    public PlainText(String str) {
        try {
            if ( str == null ) {
                throw new IllegalArgumentException("String for plaintext may not be null!");
            }
            rawBytes = str.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Should never happen.
            logger.error(Logger.EVENT_FAILURE, "PlainText(String) CTOR failed: Can't find UTF-8 byte-encoding!", e);
            throw new RuntimeException("Can't find UTF-8 byte-encoding!", e);
        }
    }

    /**
     * Construct a {@code PlainText} object from a {@code byte} array.
     * @param b    The {@code byte} array used to create the {@code PlainText}
     *             object.
     */
    public PlainText(byte[] b) {
        // Must allow 0 length arrays though, to represent empty strings.
        if ( b == null ) {
            throw new IllegalArgumentException("Byte array representing plaintext cannot be null.");
        }

        // Make copy so mutable byte array b can't change PlainText.
        rawBytes = new byte[ b.length ];
        System.arraycopy(b, 0, rawBytes, 0, b.length);
    }

    /**
     * Convert the {@code PlainText} object to a UTF-8 encoded {@code String}.
     * @return    A {@code String} representing the {@code PlainText} object.
     */
    public String toString() {
        try {
            return new String(rawBytes, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Should never happen.
            logger.error(Logger.EVENT_FAILURE, "PlainText.toString() failed: Can't find UTF-8 byte-encoding!", e);
            throw new RuntimeException("Can't find UTF-8 byte-encoding!", e);
        }
    }

    /**
     * Convert the {@code PlainText} object to a byte array.
     * @return    A byte array representing the {@code PlainText} object.
     */
    public byte[] asBytes() {
        byte[] bytes = new byte[ rawBytes.length ];
        System.arraycopy(rawBytes, 0, bytes, 0, rawBytes.length);
        return bytes;
    }

    /**
     * {@inheritDoc}
     */
    @Override public boolean equals(Object anObject) {

        if ( this == anObject ) return true;
        if ( anObject == null ) return false;
        boolean result = false;
        if ( anObject instanceof PlainText ) {
            PlainText that = (PlainText)anObject;
            result = ( that.canEqual(this) &&
                    ( this.toString().equals( that.toString() ) )
                  );
        }
        return result;
    }

    /**
     * Same as {@code this.toString().hashCode()}.
     * @return    {@code this.toString().hashCode()}.
     */
    @Override public int hashCode() {
        return this.toString().hashCode();
    }

    /**
     * Return the length of the UTF-8 encoded byte array representing this
     * object. Note that if this object was constructed with the constructor
     * {@code PlainText(String str)}, then this length might not necessarily
     * agree with {@code str.length()}.
     *
     * @return    The length of the UTF-8 encoded byte array representing this
     *             object.
     */
    public int length() {
        return rawBytes.length;
    }

    // DISCUSS: Should we set 'rawBytes' to null??? Won't make it eligible for
    //            GC unless this PlainText object is set to null which can't do
    //            from here. If we set it to null, most methods will cause
    //            NullPointerException to be thrown. Also will have to change a
    //            lot of JUnit tests.
    /**
     * First overwrite the bytes of plaintext with the character '*'.
     */
    public void overwrite() {
        CryptoHelper.overwrite( rawBytes );
        // rawBytes = null;                    // See above comment re: discussion.
    }

    /**
     * Needed for correct definition of equals for general classes.
     * (Technically not needed for 'final' classes though like this class
     * though; this will just allow it to work in the future should we
     * decide to allow * sub-classing of this class.)
     * </p><p>
     * See <a href="http://www.artima.com/lejava/articles/equality.html">
     * How to write an Equality Method in Java</a>
     * for full explanation.
     * </p>
     */
    protected boolean canEqual(Object other) {
        return (other instanceof PlainText);
    }
}
